今天是專案的最後一個 Part 啦!要來加入背景音效、爆炸音效、射擊音效,並做場景中的草地貼圖、磚塊貼圖與光源調整,另外最後會試著練習載入外部模型做個完整的收尾。
Photo by Jason Leung on Unsplash
這是本系列第 28 篇,如果還沒看過第 27 篇可以點以下連結前往:
用 Three.js 來當個創世神 (27):專案實作#16 - 遊戲流程、時間倒數、分數統計
今天是專案實作的最後一天,將一路走來的苦力怕遊戲做個最後收尾,要做的有:
音效可以說是遊戲中不可或缺的要素之一,甚至可以說是靈魂也不為過,加上了音效之後,整個遊戲似乎都活了過來。而在這個專案中筆者加入了其中三個必要的音效:
以下分別說明實作的部分。
筆者到 Youtube 聽了許多的免版權音樂後,最後選擇了一首比較史詩風格的背景音樂「Ansia Orchestra – Mercury」,程式碼的部分很簡單,就是在 game.html
加入 audio tag:
<audio id="bgm" src="./music/bgm.mp3"></audio>
並且在 game.js
中設定音量與在 pointerLockControlsModule.js
設定暫停及播放時機:
// game.js
const bgm = document.getElementById('bgm')
bgm.volume = 0.5 // 音量 50%
// pointerLockControlsModule.js
if (
document.pointerLockElement === element ||
document.mozPointerLockElement === element ||
document.webkitPointerLockElement === element
) {
...
bgm.play()
} else {
bgm.pause()
}
爆炸音效使用在 Minecraft 原作中同樣的音效,而實作方法與上面差不多,只是播放時機是在每一隻苦力怕倒地爆炸時觸發。
原本筆者找了一些免費音效檔並試著自己裁減音效,發現射擊音效要直接使用外部音檔播放長度還不可以過長,不然當射擊速度較快時,就會有幾顆子彈沒有聲音。
後來隔壁棚寫 Web Audio 鐵人的 Gary 大大表示:「射擊音效用 Tone.js 的合成器做很簡單啊!」於是他就速度地幫忙提供了一個模擬射擊的音效,感謝支援:
function initGunShotSound() {
const filter = new Tone.Filter(1800, 'lowpass').toMaster()
synth = new Tone.NoiseSynth({
noise: {
type: 'white',
playbackRate: 2
},
envelope: {
attack: 0.005,
decay: 0.1,
sustain: 0.0001,
release: 0.1
}
}).connect(filter)
}
// shooting event
window.addEventListener('click', function(e) {
if (controls.enabled == true) {
if (e.which === 1) {
// 射擊聲
synth.triggerAttackRelease('0.01') // 音效持續秒數
...
}
...
}
}
詳細的 Tone.js 介紹,在 Gary 大大的鐵人賽文章講解得非常詳細,對 Web Audio API 有興趣的讀者也可以參考他的系列文!
由於是專案最後一天了,一直想來試試載入外部模型的實作,於是到 Free3D 上面找了一個木製守望台與 冰鎮 Corona 的 .obj
檔模型載入到場景中玩玩。
// 載入守望塔模型
function createTower() {
let towerBumpMat = new THREE.MeshStandardMaterial({
metalness: 0.05,
roughness: 0.9
})
towerBumpMat.map = textureLoader.load(
'./obj/tower/textures/Wood_Tower_Col.jpg'
)
loader.load('./obj/tower/tower.obj', function(loadedMesh) {
loadedMesh.children.forEach(function(child) {
child.material = towerBumpMat
child.geometry.computeFaceNormals()
child.geometry.computeVertexNormals()
})
loadedMesh.scale.set(10, 10, 10)
loadedMesh.position.set(0, -8, 100)
loadedMesh.castShadow = true
scene.add(loadedMesh)
})
}
// 載入可樂娜模型
function createCorona() {
let coronaBumpMat = new THREE.MeshStandardMaterial({
metalness: 0.05,
roughness: 0.9
})
coronaBumpMat.map = textureLoader.load('./obj/Corona/BotellaText.jpg')
coronaBumpMat.bumpMap = textureLoader.load('./obj/Corona/BotellaText.jpg')
coronaBumpMat.bumpScale = 1
loader.load('./obj/Corona/Corona.obj', function(loadedMesh) {
loadedMesh.children.forEach(function(child) {
child.material = coronaBumpMat
child.geometry.computeFaceNormals()
child.geometry.computeVertexNormals()
})
// loadedMesh.scale.set(0.2, 0.2, 0.2)
loadedMesh.position.set(30, 0, 100)
loadedMesh.rotation.x = -0.3
loadedMesh.rotation.y = 0.5
loadedMesh.rotation.z = -0.5
loadedMesh.castShadow = true
scene.add(loadedMesh)
})
}
在 Three.js 中可以載入許多種檔案類型的外部模型,這應該也是一般開發中常用的建模方法,而筆者在這邊只實驗 .obj
檔的載入,需要引入額外的函式庫 OBJLoader.js
(官方下載網址)即可使用。
比較可惜的是,這個只是網格模型,沒有剛體效果,如果這個守望塔能做成剛體效果一定很好玩,稍微查了一下查到一篇 Cannon.js 作者在 Stackoverflow 上回答的文章,作者表示可以透過他寫的 Trimesh 類別來做碰撞偵測,但僅止支援球體與平面的碰撞。
而一般比較有效率的做法會將模型載到 Unity、PlayCanvas 等內建支援 WebGL 物理引擎的軟體做物理效果。
場景優化的部分,參考官方衣服飄動的範例將地板貼上了草皮貼圖:
const groundTexture = textureLoader.load('./img/grasslight-big.jpg')
groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping
groundTexture.repeat.set(25, 25)
groundTexture.anisotropy = 16
const groundMaterial = new THREE.MeshLambertMaterial({ map: groundTexture })
右鍵磚塊的部分,參考 K 大大當初的教學將磚塊補上了凹凸貼圖的效果:
let brickBumpMat = new THREE.MeshStandardMaterial({
metalness: 0.1,
roughness: 0.8
})
brickBumpMat.map = textureLoader.load('./img/brickNormal.jpg')
brickBumpMat.bumpMap = textureLoader.load('./img/brickBumpMap.jpg')
brickBumpMat.bumpScale = 1
const brickMesh = new THREE.Mesh(boxGeometry, brickBumpMat)
不過因為圖片載入似乎會有稍微的延遲,在快速點右鍵時,看起來有一點閃爍不太舒服,有點考慮是不是要優化或拿掉。
光源的部分加上了平行光及半球光補強:
let directionalLight = new THREE.DirectionalLight(0x555555)
directionalLight.position.set(100, 100, 100)
scene.add(directionalLight)
let hemiLight = new THREE.HemisphereLight(0x000022, 0x002200, 0.5)
hemiLight.position.set(0, 300, 0)
scene.add(hemiLight)
另外也順手讓苦力怕站的分開一點,將時間上調至四分鐘(因為 BGM 大約四分鐘懶得剪 XD),場地加大,讓喜歡散步的玩家可以有多一點活動的空間。
專案也終於在上面的補強後告一個段落,而可以優化的地方仍然相當地多,像是走路音效、排行榜、時間倒數提醒、遊戲結束的提示與過場載入畫面、I18N、一頁式遊戲流程、苦力怕在所屬領地內隨機走動、苦力怕攻擊玩家等等,還有太多的細節與實作可以發揮,但遊戲的第一版,就用一瓶 Corona 暫時畫下句點吧。
想當初在「專案實作#1」時還沒什麼信心可以完成遊戲專案,那時才掌握了基本的建模與動畫,對粒子系統與物理引擎都不熟悉的情況下,可以一路上跌跌撞撞,最後完成作品還是蠻感動的。
只能說親自體驗到,做遊戲真的不是一件容易的事,尤其是一人獨力完成開發,又是時間有限的情況下,只能不斷的取捨功能要不要做、 bug 要不要解,下次有機會再做遊戲的話,應該組個隊來開發才是。
今天將專案做了個大收尾,加上了遊戲音效,並且將場景的貼圖及光源做了優化,並載入幾個外部模型到場景中做裝飾,順利在鐵人賽的最後幾天內,將整個苦力怕射擊遊戲專案完成,明天會來做整個系列文的總結,並分享相關的延伸學習資源,我們明天見!
太強啦~~~
難度好高,打到 15 隻就緊繃了 XD
我自己實測後,最高只打了 16 隻,難度看起來剛好,蠻有挑戰性的